/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.ldap.ppolicy;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import netscape.ldap.ber.stream.BERChoice;
import netscape.ldap.ber.stream.BERElement;
import netscape.ldap.ber.stream.BEREnumerated;
import netscape.ldap.ber.stream.BERInteger;
import netscape.ldap.ber.stream.BERIntegral;
import netscape.ldap.ber.stream.BERSequence;
import netscape.ldap.ber.stream.BERTag;
import netscape.ldap.ber.stream.BERTagDecoder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.dao.DataRetrievalFailureException;

/**
 * Represents the response control received when a <tt>PasswordPolicyControl</tt> is used
 * when binding to a directory. Currently tested with the OpenLDAP 2.3.19 implementation
 * of the LDAP Password Policy Draft. It extends the request control with the control
 * specific data. This is accomplished by the properties <tt>timeBeforeExpiration</tt>,
 * <tt>graceLoginsRemaining</tt>.
 * <p>
 *
 *
 * @author Stefan Zoerner
 * @author Luke Taylor
 *
 * @see PasswordPolicyControl
 * @see <a href="https://www.ibm.com/developerworks/tivoli/library/t-ldap-controls/">Stefan
 * Zoerner's IBM developerworks article on LDAP controls.</a>
 */
public class PasswordPolicyResponseControl extends PasswordPolicyControl {
    // ~ Static fields/initializers
    // =====================================================================================

    private static final Log logger = LogFactory
            .getLog(PasswordPolicyResponseControl.class);

    // ~ Instance fields
    // ================================================================================================

    private final byte[] encodedValue;

    private PasswordPolicyErrorStatus errorStatus;

    private int graceLoginsRemaining = Integer.MAX_VALUE;
    private int timeBeforeExpiration = Integer.MAX_VALUE;

    // ~ Constructors
    // ===================================================================================================

    /**
     * Decodes the Ber encoded control data. The ASN.1 value of the control data is:
     *
     * <pre>
     *    PasswordPolicyResponseValue ::= SEQUENCE {       warning [0] CHOICE {
     *           timeBeforeExpiration [0] INTEGER (0 .. maxInt),
     *           graceAuthNsRemaining [1] INTEGER (0 .. maxInt) } OPTIONAL,       error   [1] ENUMERATED {
     *           passwordExpired             (0),          accountLocked               (1),
     *           changeAfterReset            (2),          passwordModNotAllowed       (3),
     *           mustSupplyOldPassword       (4),          insufficientPasswordQuality (5),
     *           passwordTooShort            (6),          passwordTooYoung            (7),
     *           passwordInHistory           (8) } OPTIONAL }
     * </pre>
     *
     */
    public PasswordPolicyResponseControl(byte[] encodedValue) {
        this.encodedValue = encodedValue;

        // PPolicyDecoder decoder = new JLdapDecoder();
        PPolicyDecoder decoder = new NetscapeDecoder();

        try {
            decoder.decode();
        }
        catch (IOException e) {
            throw new DataRetrievalFailureException("Failed to parse control value", e);
        }
    }

    // ~ Methods
    // ========================================================================================================

    /**
     * Returns the unchanged value of the response control. Returns the unchanged value of
     * the response control as byte array.
     */
    @Override
    public byte[] getEncodedValue() {
        return this.encodedValue;
    }

    public PasswordPolicyErrorStatus getErrorStatus() {
        return this.errorStatus;
    }

    /**
     * Returns the graceLoginsRemaining.
     *
     * @return Returns the graceLoginsRemaining.
     */
    public int getGraceLoginsRemaining() {
        return this.graceLoginsRemaining;
    }

    /**
     * Returns the timeBeforeExpiration.
     *
     * @return Returns the time before expiration in seconds
     */
    public int getTimeBeforeExpiration() {
        return this.timeBeforeExpiration;
    }

    /**
     * Checks whether an error is present.
     *
     * @return true, if an error is present
     */
    public boolean hasError() {
        return this.errorStatus != null;
    }

    /**
     * Checks whether a warning is present.
     *
     * @return true, if a warning is present
     */
    public boolean hasWarning() {
        return (this.graceLoginsRemaining != Integer.MAX_VALUE)
                || (this.timeBeforeExpiration != Integer.MAX_VALUE);
    }

    public boolean isExpired() {
        return this.errorStatus == PasswordPolicyErrorStatus.PASSWORD_EXPIRED;
    }

    public boolean isChangeAfterReset() {
        return this.errorStatus == PasswordPolicyErrorStatus.CHANGE_AFTER_RESET;
    }

    public boolean isUsingGraceLogins() {
        return this.graceLoginsRemaining < Integer.MAX_VALUE;
    }

    /**
     * Determines whether an account locked error has been returned.
     *
     * @return true if the account is locked.
     */
    public boolean isLocked() {
        return this.errorStatus == PasswordPolicyErrorStatus.ACCOUNT_LOCKED;
    }

    /**
     * Create a textual representation containing error and warning messages, if any are
     * present.
     *
     * @return error and warning messages
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder("PasswordPolicyResponseControl");

        if (hasError()) {
            sb.append(", error: ").append(this.errorStatus.getDefaultMessage());
        }

        if (this.graceLoginsRemaining != Integer.MAX_VALUE) {
            sb.append(", warning: ").append(this.graceLoginsRemaining)
                    .append(" grace logins remain");
        }

        if (this.timeBeforeExpiration != Integer.MAX_VALUE) {
            sb.append(", warning: time before expiration is ")
                    .append(this.timeBeforeExpiration);
        }

        if (!hasError() && !hasWarning()) {
            sb.append(" (no error, no warning)");
        }

        return sb.toString();
    }

    // ~ Inner Interfaces
    // ===============================================================================================

    private interface PPolicyDecoder {
        void decode() throws IOException;
    }

    // ~ Inner Classes
    // ==================================================================================================

    /**
     * Decoder based on Netscape ldapsdk library
     */
    private class NetscapeDecoder implements PPolicyDecoder {
        public void decode() throws IOException {
            int[] bread = { 0 };
            BERSequence seq = (BERSequence) BERElement
                    .getElement(new SpecificTagDecoder(),
                            new ByteArrayInputStream(
                                    PasswordPolicyResponseControl.this.encodedValue),
                            bread);

            int size = seq.size();

            if (logger.isDebugEnabled()) {
                logger.debug("PasswordPolicyResponse, ASN.1 sequence has " + size
                        + " elements");
            }

            for (int i = 0; i < seq.size(); i++) {
                BERTag elt = (BERTag) seq.elementAt(i);

                int tag = elt.getTag() & 0x1F;

                if (tag == 0) {
                    BERChoice warning = (BERChoice) elt.getValue();

                    BERTag content = (BERTag) warning.getValue();
                    int value = ((BERInteger) content.getValue()).getValue();

                    if ((content.getTag() & 0x1F) == 0) {
                        PasswordPolicyResponseControl.this.timeBeforeExpiration = value;
                    }
                    else {
                        PasswordPolicyResponseControl.this.graceLoginsRemaining = value;
                    }
                }
                else if (tag == 1) {
                    BERIntegral error = (BERIntegral) elt.getValue();
                    PasswordPolicyResponseControl.this.errorStatus = PasswordPolicyErrorStatus
                            .values()[error.getValue()];
                }
            }
        }

        class SpecificTagDecoder extends BERTagDecoder {
            /** Allows us to remember which of the two options we're decoding */
            private Boolean inChoice = null;

            @Override
            public BERElement getElement(BERTagDecoder decoder, int tag,
                                         InputStream stream, int[] bytesRead, boolean[] implicit)
                    throws IOException {
                tag &= 0x1F;
                implicit[0] = false;

                if (tag == 0) {
                    // Either the choice or the time before expiry within it
                    if (this.inChoice == null) {
                        setInChoice(true);

                        // Read the choice length from the stream (ignored)
                        BERElement.readLengthOctets(stream, bytesRead);

                        int[] componentLength = new int[1];
                        BERElement choice = new BERChoice(decoder, stream,
                                componentLength);
                        bytesRead[0] += componentLength[0];

                        // inChoice = null;
                        return choice;
                    }
                    else {
                        // Must be time before expiry
                        return new BERInteger(stream, bytesRead);
                    }
                }
                else if (tag == 1) {
                    // Either the graceLogins or the error enumeration.
                    if (this.inChoice == null) {
                        // The enumeration
                        setInChoice(false);

                        return new BEREnumerated(stream, bytesRead);
                    }
                    else {
                        if (this.inChoice.booleanValue()) {
                            // graceLogins
                            return new BERInteger(stream, bytesRead);
                        }
                    }
                }

                throw new DataRetrievalFailureException("Unexpected tag " + tag);
            }

            private void setInChoice(boolean inChoice) {
                this.inChoice = Boolean.valueOf(inChoice);
            }
        }
    }

    /** Decoder based on the OpenLDAP/Novell JLDAP library */

    // private class JLdapDecoder implements PPolicyDecoder {
    //
    // public void decode() throws IOException {
    //
    // LBERDecoder decoder = new LBERDecoder();
    //
    // ASN1Sequence seq = (ASN1Sequence)decoder.decode(encodedValue);
    //
    // if(seq == null) {
    //
    // }
    //
    // int size = seq.size();
    //
    // if(logger.isDebugEnabled()) {
    // logger.debug("PasswordPolicyResponse, ASN.1 sequence has " +
    // size + " elements");
    // }
    //
    // for(int i=0; i < size; i++) {
    //
    // ASN1Tagged taggedObject = (ASN1Tagged)seq.get(i);
    //
    // int tag = taggedObject.getIdentifier().getTag();
    //
    // ASN1OctetString value = (ASN1OctetString)taggedObject.taggedValue();
    // byte[] content = value.byteValue();
    //
    // if(tag == 0) {
    // parseWarning(content, decoder);
    //
    // } else if(tag == 1) {
    // // Error: set the code to the value
    // errorCode = content[0];
    // }
    // }
    // }
    //
    // private void parseWarning(byte[] content, LBERDecoder decoder) {
    // // It's the warning (choice). Parse the number and set either the
    // // expiry time or number of logins remaining.
    // ASN1Tagged taggedObject = (ASN1Tagged)decoder.decode(content);
    // int contentTag = taggedObject.getIdentifier().getTag();
    // content = ((ASN1OctetString)taggedObject.taggedValue()).byteValue();
    // int number;
    //
    // try {
    // number = ((Long)decoder.decodeNumeric(new ByteArrayInputStream(content),
    // content.length)).intValue();
    // } catch(IOException e) {
    // throw new LdapDataAccessException("Failed to parse number ", e);
    // }
    //
    // if(contentTag == 0) {
    // timeBeforeExpiration = number;
    // } else if (contentTag == 1) {
    // graceLoginsRemaining = number;
    // }
    // }
    // }
}
